﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace ESP.Data
{
    public abstract class CommandParameters
    {
        public Dictionary<string, string> SubParameters { get; private set; }

        public List<SqlParameter> Parameters { get; private set; }

        public string Token { get; private set; }

        public CommandParameters(string token)
        {
            this.Token = token;
            this.SubParameters = new Dictionary<string, string>();
            this.Parameters = new List<SqlParameter>();
        }

        protected internal abstract void Build();


        protected static Dictionary<ParameterExpression, string> GetParameterTableNameMappings(ReadOnlyCollection<ParameterExpression> parameters, Dictionary<object, string> tableMappings)
        {
            var parameterMappings = new Dictionary<ParameterExpression, string>();

            if (parameters == null || parameters.Count == 0 || tableMappings == null || tableMappings.Count == 0)
                return parameterMappings;

            for (int i = 0; i < parameters.Count; i++)
            {
                var p = parameters[i];
                string tableName;
                if (tableMappings.TryGetValue(i, out tableName) || tableMappings.TryGetValue(p.Type, out tableName))
                    parameterMappings.Add(p, tableName);
            }
            return parameterMappings;
        }

        protected static string QuoteName(string name)
        {
            if (!name.StartsWith("["))
                name = "[" + name;
            if (!name.EndsWith("]"))
                name += "]";

            return name;
        }

        protected void TranslateExpression(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var nodeType = expression.NodeType;

            switch (nodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    BuildBinary(builder, expression, parameterMappings, "AND");
                    break;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    BuildBinary(builder, expression, parameterMappings, "OR");
                    break;
                case ExpressionType.Equal:
                    BuildBinary(builder, expression, parameterMappings, "=");
                    break;
                case ExpressionType.LessThan:
                    BuildBinary(builder, expression, parameterMappings, "<");
                    break;
                case ExpressionType.LessThanOrEqual:
                    BuildBinary(builder, expression, parameterMappings, "<=");
                    break;
                case ExpressionType.GreaterThan:
                    BuildBinary(builder, expression, parameterMappings, ">");
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    BuildBinary(builder, expression, parameterMappings, ">=");
                    break;
                case ExpressionType.NotEqual:
                    BuildBinary(builder, expression, parameterMappings, "<>");
                    break;

                case ExpressionType.Add:
                case ExpressionType.AddChecked:
                    BuildBinary(builder, expression, parameterMappings, "+");
                    break;
                case ExpressionType.Divide:
                    BuildBinary(builder, expression, parameterMappings, "/");
                    break;
                case ExpressionType.Multiply:
                case ExpressionType.MultiplyChecked:
                    BuildBinary(builder, expression, parameterMappings, "*");
                    break;
                case ExpressionType.Subtract:
                case ExpressionType.SubtractChecked:
                    BuildBinary(builder, expression, parameterMappings, "-");
                    break;

                case ExpressionType.Not:
                    BuildNot(builder, expression, parameterMappings);
                    break;
                case ExpressionType.Negate:
                case ExpressionType.NegateChecked:
                    BuildNegate(builder, expression, parameterMappings);
                    break;

                case ExpressionType.ExclusiveOr:
                    BuildBinary(builder, expression, parameterMappings, "~");
                    break;

                case ExpressionType.Coalesce:
                    BuildCoalesce(builder, expression, parameterMappings);
                    break;
                case ExpressionType.MemberAccess:
                    BuildMemberAccess(builder, expression, parameterMappings);
                    break;
                case ExpressionType.Constant:
                    BuildConstant(builder, expression);
                    break;
                case ExpressionType.Call:
                    BuildCall(builder, expression, parameterMappings);
                    break;
                case ExpressionType.Convert:
                case ExpressionType.ConvertChecked:
                    BuildConvert(builder, expression, parameterMappings);
                    break;
                default:
                    BuildConstant(builder, GetExpressionValue(expression));
                    break;
            }
        }

        private void BuildConvert(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var unary = (UnaryExpression)expression;
            var operand = unary.Operand;
            if (operand is MemberExpression)
            {
                var member = (MemberExpression)operand;
                if (member.Expression is ParameterExpression)
                {
                    BuildMemberAccess(builder, member, parameterMappings);
                    return;
                }
            }

            BuildConstant(builder, GetExpressionValue(unary));
        }

        private void BuildNegate(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var ue = (UnaryExpression)expression;

            builder.Append("(-");
            TranslateExpression(builder, ue.Operand, parameterMappings);
            builder.Append(")");
        }

        private void BuildNot(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var ue = (UnaryExpression)expression;

            builder.Append("(NOT ");
            TranslateExpression(builder, ue.Operand, parameterMappings);
            builder.Append(")");
        }

        private void BuildCall(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var callExpression = (MethodCallExpression)expression;
            var method = callExpression.Method;
            if (method.DeclaringType == typeof(string) && (method.Name == "Contains" || method.Name == "StartWith" || method.Name == "EndWith"))
            {
                BuildStringLike(builder, callExpression.Object, callExpression.Arguments[0], method.Name, parameterMappings);
            }
            else if (method.Name == "Contains" && method.DeclaringType == typeof(System.Linq.Enumerable))
            {
                BuildIn(builder, callExpression.Arguments[0], callExpression.Arguments[1], parameterMappings);
            }
            else
            {
                BuildConstant(builder, GetExpressionValue(expression));
            }
        }

        private void BuildIn(StringBuilder builder, Expression targetObject, Expression parameter, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var enumerable = GetExpressionValue(targetObject) as System.Collections.IEnumerable;

            List<object> elements = new List<object>();

            if (enumerable != null)
            {
                foreach (var i in enumerable)
                {
                    elements.Add(i);
                }
            }

            if (elements.Count == 0)
            {
                builder.Append("(1<>1)");
                return;
            }

            builder.Append("(");

            TranslateExpression(builder, parameter, parameterMappings);
            builder.Append(" IN (");
            BuildConstant(builder, elements[0]);

            for (int i = 1; i < elements.Count; i++)
            {
                builder.Append(",");
                BuildConstant(builder, elements[i]);
            }

            builder.Append("))");
        }

        private void BuildStringLike(StringBuilder builder, Expression targetObject, Expression parameter, string methedName, Dictionary<ParameterExpression, string> parameterMappings)
        {
            builder.Append("(");
            TranslateExpression(builder, targetObject, parameterMappings);
            builder.Append(" LIKE (");

            if (methedName == "StartWith" || methedName == "Contains")
                builder.Append("'%' + ");

            TranslateExpression(builder, parameter, parameterMappings);

            if (methedName == "EndWith" || methedName == "Contains")
                builder.Append(" + '%'");

            builder.Append("))");
        }

        private void BuildMemberAccess(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var memberExpression = (MemberExpression)expression;
            var targetExpression = memberExpression.Expression;
            if (targetExpression is ParameterExpression)
            {
                var parameter = (ParameterExpression)targetExpression;
                string tableName;
                if (parameterMappings.TryGetValue(parameter, out tableName))
                {
                    builder.Append(QuoteName(tableName));
                    builder.Append(".");
                }
                builder.Append("[").Append(memberExpression.Member.Name).Append("]");
            }
            else
            {
                BuildConstant(builder, GetExpressionValue(expression));
            }
        }

        private void BuildCoalesce(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings)
        {
            var binaryExpression = (BinaryExpression)expression;
            builder.Append("ISNULL(");
            TranslateExpression(builder, binaryExpression.Left, parameterMappings);
            builder.Append(",");
            TranslateExpression(builder, binaryExpression.Right, parameterMappings);
            builder.Append(")");
        }

        private static object GetExpressionValue(Expression expression)
        {
            var lambada = System.Linq.Expressions.Expression.Lambda(expression);
            var obj = lambada.Compile().DynamicInvoke();
            return obj;
        }

        private void BuildBinary(StringBuilder builder, Expression expression, Dictionary<ParameterExpression, string> parameterMappings, string op)
        {
            var binaryExpression = (BinaryExpression)expression;
            builder.Append("(");

            if (op == "=" || op == "<>")
            {

                int length00 = builder.Length;

                TranslateExpression(builder, binaryExpression.Left, parameterMappings);

                int length01 = builder.Length;

                if (IsNull(builder, length00, length01))
                {
                    builder.Length = length00;

                    TranslateExpression(builder, binaryExpression.Right, parameterMappings);

                    length01 = builder.Length;

                    if (IsNull(builder, length00, length01))
                    {
                        builder.Length = length00;
                        builder.Append(op == "=" ? "1=1" : "1<>1");
                    }
                    else
                    {
                        builder.Append(op == "=" ? " IS NULL" : " IS NOT NULL");
                    }
                }
                else
                {

                    builder.Append(" ").Append(op).Append(" ");

                    int length02 = builder.Length;

                    TranslateExpression(builder, binaryExpression.Right, parameterMappings);

                    int length03 = builder.Length;

                    if (IsNull(builder, length02, length03))
                    {
                        builder.Length = length01;
                        builder.Append(op == "=" ? " IS NULL" : " IS NOT NULL");
                    }
                }
            }
            else if (op == "AND" || op == "OR")
            {
                TranslateExpression(builder, binaryExpression.Right, parameterMappings);
                builder.Append(" ").Append(op).Append(" ");
                TranslateExpression(builder, binaryExpression.Left, parameterMappings);
            }
            else
            {
                TranslateExpression(builder, binaryExpression.Left, parameterMappings);
                builder.Append(" ").Append(op).Append(" ");
                TranslateExpression(builder, binaryExpression.Right, parameterMappings);
            }
            builder.Append(")");
        }

        private static bool IsNull(StringBuilder builder, int length1, int length2)
        {
            return (length2 - length1) == 4 && builder[length1] == 'N' && builder[length1 + 1] == 'U' && builder[length1 + 2] == 'L' && builder[length1 + 3] == 'L';
        }

        private void BuildConstant(StringBuilder builder, Expression expression)
        {
            var constantExpression = (ConstantExpression)expression;
            var value = constantExpression.Value;
            BuildConstant(builder, value);
        }

        protected void BuildConstant(StringBuilder builder, object value)
        {
            if (value == null)
            {
                builder.Append("NULL");
                return;
            }

            if (value is Enum)
            {
                value = Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()));
            }

            var p = new SqlParameter(GetNextParameterName(), value);
            Parameters.Add(p);
            builder.Append(p.ParameterName);
        }

        protected string GetNextParameterName()
        {
            return "@" + CommandBuilder.ParameterNamePrefix
                + "_" + Token + "_"
                + Parameters.Count.ToString(System.Globalization.NumberFormatInfo.InvariantInfo);
        }
    }

}
